package com.bjy.qa.service.functionaltest.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bjy.qa.dao.functionaltest.AgentDao;
import com.bjy.qa.dao.functionaltest.CatalogDao;
import com.bjy.qa.dao.functionaltest.StepDao;
import com.bjy.qa.dao.functionaltest.TestCaseDao;
import com.bjy.qa.entity.functionaltest.*;
import com.bjy.qa.entity.tools.FileInfo;
import com.bjy.qa.enumtype.*;
import com.bjy.qa.exception.MyException;
import com.bjy.qa.service.functionaltest.*;
import com.bjy.qa.service.tools.ITaskManagerService;
import com.bjy.qa.util.file.FileTools;
import com.bjy.qa.util.security.SecurityUtils;
import com.rslai.commons.util.excel.ExcelRead;
import com.rslai.commons.util.excel.ExcelWrite;
import com.rslai.commons.util.excel.Head;
import com.rslai.commons.util.excel.exception.ExcelException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.io.*;
import java.util.*;

@Service
public class TestCaseService extends ServiceImpl<TestCaseDao, TestCase> implements ITestCaseService {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Resource
    TestCaseDao testCaseDao;

    @Resource
    StepDao stepDao;

    @Resource
    IStepService iStepService;

    @Resource
    ICatalogService iCatalogService;

    @Resource
    ITaskManagerService iTaskManagerService;

    @Resource
    ITestCaseHistoryService iTestCaseHistoryService;

    @Resource
    CatalogDao catalogDao;

    @Resource
    ITestSuiteService iTestSuiteService;

    @Resource
    AgentDao agentDao;

    @Resource
    IAgentService iAgentService;

    @Override
    public TestCase add(TestCase testCase) {
        testCase.setDesigner(SecurityUtils.getCurrentUserName());
        testCase.setCreatedAt(null);
        testCase.setUpdatedAt(null);
        testCaseDao.insert(testCase);

        this.recursionSaveOrUpdateStep(testCase.getId(), 0L, testCase.getSteps()); // 递归 保存或更新步骤列表

        iTestCaseHistoryService.add(testCase); // 添加历史记录

        return testCase;
    }

    @Override
    public TestCase update(TestCase testCase) {
        testCase.setDesigner(SecurityUtils.getCurrentUserName());
        testCase.setCreatedAt(null);
        testCase.setUpdatedAt(null);
        testCaseDao.updateById(testCase);

        this.recursionSaveOrUpdateStep(testCase.getId(), 0L, testCase.getSteps()); // 递归 保存或更新步骤列表

        iTestCaseHistoryService.add(testCase); // 添加历史记录

        return testCase;
    }

    /**
     * 递归 保存或更新步骤列表
     * @param testCaseId 测试用例 ID（这些 step 属于那个 测试用例）
     * @param parentId step 的 父 ID
     * @param stepList 步骤列表
     * @return
     */
    private boolean recursionSaveOrUpdateStep(Long testCaseId, Long parentId, List<Step> stepList) {
        /**
         * 保存或更新 当前层级的 步骤列表
         */
        // 遍历 steps，按照有没有 stepId 先将新增的和更新的分开
        List<Step> newSteps = new ArrayList<Step>(); // 需要新增的 step
        List<Step> tempUpdateSteps = new ArrayList<Step>(); // 临时保存，存在的需要更新的 step（虽然有 id 存在，但也有可能被其他人删除）
        int index = 0;
        for (Step step : stepList) {
            if (step.getId() == null || step.getId() <= 0) { // 新增
                step.setId(null);
                step.setTestCaseId(testCaseId);
                newSteps.add(step);
            } else { // 更新
                tempUpdateSteps.add(step);
            }
            step.setParentId(parentId);
            step.setCreatedAt(null);
            step.setUpdatedAt(null);
            step.setSort(++index);
        }

        // 遍历 临时保存，存在的需要更新的 step。如果存在，就放到 updateSteps 中，更新。否则放到 newSteps 中，新增
        List<Long> dataStepIds = stepDao.getListByTestCaseId(testCaseId, parentId); // 根据测试用例 id 和  父 ID，获得当前库中的 step 列表
        List<Step> updateSteps = new ArrayList<Step>();
        for (Step step : tempUpdateSteps) {
            if (dataStepIds.remove(step.getId())) {
                updateSteps.add(step);
            } else {
                newSteps.add(step);
            }
        }

        // 如果 dataStepIds 中还有，需要删除（可能是恢复历史记录时不存在，但恢复前存在。例如：当前版本有两个 step，但上个版本只有一个 step，这样恢复上个版本，如果不执行这个删除，那恢复后还是两个 step）
        if (dataStepIds.size() > 0) {
            iStepService.deletePhysically(dataStepIds);
        }

        iStepService.saveBatch(newSteps);
        iStepService.updateBatchById(updateSteps);


        /**
         * 循环 保存或更新 下一层级的 步骤列表
         */
        for (Step step : newSteps) {
            if (step.getChildSteps() != null && step.getChildSteps().size() > 0) {
                this.recursionSaveOrUpdateStep(testCaseId, step.getId(), step.getChildSteps());
            }
        }

        for (Step step : updateSteps) {
            if (step.getChildSteps() != null && step.getChildSteps().size() > 0) {
                this.recursionSaveOrUpdateStep(testCaseId, step.getId(), step.getChildSteps());
            } else {
                stepDao.deletePhysicallyByParentId(step.getId()); // 删除 这个id的父 id
            }
        }

        return true;
    }

    @Override
    public List<TestCase> saveOrUpdateBatch(List<TestCase> testCases) {
        int index = 0;
        for (TestCase testCase : testCases) {
            testCase.setSort(++index);
            testCase.setDesigner(SecurityUtils.getCurrentUserName());
            testCase.setCreatedAt(null);
            testCase.setUpdatedAt(null);
            this.saveOrUpdate(testCase);
            for (Step step : testCase.getSteps()) {
                if (step.getId() == 0) {
                    step.setTestCaseId(testCase.getId());
                    step.setCreatedAt(null);
                    step.setUpdatedAt(null);
                }
            }
            iStepService.saveOrUpdateBatch(testCase.getSteps()) ;

            iTestCaseHistoryService.add(testCase); // 添加历史记录
        }

        return testCases;
    }

    @Override
    public TestCase selectById(Long id) {
        return testCaseDao.selectById(id);
    }

    @Override
    public List<TestCase> selectByCatalogId(Long projectId, Long cid) {
        return testCaseDao.selectByCatalogId(projectId, CatalogType.TEST_CASE, cid);
    }

    @Override
    public int delete(Long id) {
        // 删除 step
        QueryWrapper<Step> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("test_case_id", id);
        stepDao.delete(queryWrapper);

        return testCaseDao.deleteById(id); // 删除 api
    }

    @Async
    @Override
    @Transactional(timeout = 600)
    public Object fileImport(String taskId, String filePath, Long projectId, String userName) {
        iTaskManagerService.changeTaskStatus(taskId, TaskStatus.RUN); // 修改任务状态为运行中

        InputStream is = null;
        OutputStream os = null;
        try {
            // 生成保存错误信息 excel
            ExcelWrite excelWrite = new ExcelWrite();
            excelWrite.addHead(new Head(0, "用例编号", 20, null));
            excelWrite.addHead(new Head(1, "用例分类", 50, null));
            excelWrite.addHead(new Head(2, "优先级", 8, null));
            excelWrite.addHead(new Head(3, "用例名称", 30, null));
            excelWrite.addHead(new Head(4, "前置条件", 20, null));
            excelWrite.addHead(new Head(5, "操作步骤", 30, null));
            excelWrite.addHead(new Head(6, "预期结果", 30, null));
            excelWrite.addHead(new Head(7, "设计者", 8, null));

            String currentCatalog = null; // 当前分类
            List<TestCase> caseList = new LinkedList<>(); // 用例列表
            List<Step> stepList = new LinkedList<>(); // 步骤列表

            is = new FileInputStream(filePath);
            ExcelRead excelRead = new ExcelRead(is);
            while (excelRead.hasNext()) {
                try {
                    excelRead.next();
                    String catalog = excelRead.getString("用例分类");
                    if (currentCatalog == null || currentCatalog.equals(catalog)) { // 分类相同 或 currentCatalog为空（第一行数据），不需要插入数据，组装 testList 和 stepList
                        currentCatalog = catalog;
                        fileImportAssemblyData(excelRead, caseList, stepList, projectId, userName); // 导入测试用例，组装数据
                    } else { // 分类不同，插入 分类、testList、stepList
                        fileImportSaveData(currentCatalog, projectId, caseList, stepList); // 导入测试用例，保存数据

                        // 分类不同也要把当前行数据存入 testList 和 stepList。以备下一次循环使用
                        caseList.clear();
                        stepList.clear();
                        currentCatalog = catalog;
                        fileImportAssemblyData(excelRead, caseList, stepList, projectId, userName); // 导入测试用例，组装数据
                    }
                } catch (ExcelException e) {
                    // 有错误，将错误数据和错误信息保存到 excel 文件
                    excelWrite.addBody(excelRead.getCurrentData());
                    excelWrite.addCommentCurrentRow(e.getMessage());
                }
            }

            // 当最后一行，在循环内不能保存数据，则保存数据
            if (caseList.size() > 0) {
                fileImportSaveData(currentCatalog, projectId, caseList, stepList);
            }

            // 保存失败的数据到 excel 文件
            if (excelWrite.getTotalRow() > 1) { // 有错误数据（1：excel 中只有表头）
                FileInfo fileInfo = FileTools.createFile(".xls", true);
                os = new FileOutputStream(fileInfo.getFile());
                excelWrite.write(os);
                iTaskManagerService.changeTaskStatus(taskId, TaskStatus.END_BREAK, "测试用例部分导入失败", fileInfo.getUrl());
            }
        } catch (FileNotFoundException e) {
            iTaskManagerService.changeTaskStatus(taskId, TaskStatus.END_FAILED, "测试用例导入失败，文件未找到。");
            logger.error("测试用例导入失败", e);
            e.printStackTrace();
        } catch (Exception e) {
            iTaskManagerService.changeTaskStatus(taskId, TaskStatus.END_FAILED, "测试用例导入失败。" + e.getMessage());
            logger.error("测试用例导入失败", e);
            e.printStackTrace();
        } finally {
            if (null != is) {
                try {
                    if (is != null) {
                        is.close();
                    }
                    if (os != null) {
                        os.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        if (iTaskManagerService.getTask(taskId).getStatus().getValue() < TaskStatus.END_BREAK.getValue()) {
            iTaskManagerService.changeTaskStatus(taskId, TaskStatus.END_SUCCESS);
        } else if (iTaskManagerService.getTask(taskId).getStatus().equals(TaskStatus.END_FAILED)) {
            throw new RuntimeException("导入测试用例，任务失败");
        }
        return true;
    }

    @Async
    @Override
    @Transactional(timeout = 600)
    public Boolean export(String taskId, ExportDTO exportDTO) {
        switch (exportDTO.getFileType()) {
            case EXCEL:
                return exportExcel(taskId, exportDTO);
            default:
                throw new RuntimeException("导出测试用例，不支持的文件类型");
        }
    }

    /**
     * 导出 excel
     * @param taskId 任务id
     * @param exportDTO 导出 DTO 模型
     * @return
     */
    private Boolean exportExcel(String taskId, ExportDTO exportDTO) {
        iTaskManagerService.changeTaskStatus(taskId, TaskStatus.RUN); // 修改任务状态为运行中

        OutputStream os = null;
        try {
            Map<String, List<TestCase>> exportData = new HashMap<>(); // 需要导出的所有数据
            recursiveExportDTO("", exportDTO.getTree(), exportData); // 递归 ExportDTO，取得需要导出的所有数据

            // 生成导出的 excel
            ExcelWrite excelWrite = new ExcelWrite();
            excelWrite.addHead(new Head(0, "用例编号", 20, null));
            excelWrite.addHead(new Head(1, "用例分类", 50, null));
            excelWrite.addHead(new Head(2, "优先级", 8, null));
            excelWrite.addHead(new Head(3, "用例名称", 30, null));
            excelWrite.addHead(new Head(4, "前置条件", 20, null));
            excelWrite.addHead(new Head(5, "操作步骤", 30, null, true));
            excelWrite.addHead(new Head(6, "预期结果", 30, null, true));
            excelWrite.addHead(new Head(7, "设计者", 8, null));

            for (Map.Entry<String, List<TestCase>> entry: exportData.entrySet()) {
                for (TestCase testCase : entry.getValue()) {
                    String caseNumber = testCase.getNumber(); // 用例编号
                    String catalog = entry.getKey(); // 用例分类
                    String priority = testCase.getPriority().getName(); // 优先级
                    String caseName = testCase.getName(); // 用例名称
                    String before = testCase.getBefore(); // 前置条件
                    String caseStep = ""; // 操作步骤
                    String caseSulit = ""; // 预期结果
                    if (testCase.getSteps() != null && testCase.getSteps().size() > 0) {
                        caseStep = testCase.getSteps().get(0).getExtra1();
                        caseSulit = testCase.getSteps().get(0).getExtra2();
                    }
                    String designer = testCase.getDesigner(); // 设计者

                    String[] rowData = {caseNumber, catalog, priority, caseName, before, caseStep, caseSulit, designer};
                    excelWrite.addBody(rowData);
                }
            }

            FileInfo fileInfo = FileTools.createFile(".xls", true);
            os = new FileOutputStream(fileInfo.getFile());
            excelWrite.write(os);
            iTaskManagerService.changeTaskStatus(taskId, TaskStatus.END_SUCCESS, "测试用例导出成功", fileInfo.getUrl());
        } catch (Exception e) {
            iTaskManagerService.changeTaskStatus(taskId, TaskStatus.END_FAILED, "测试用例导出失败。" + e.getMessage());
            logger.error("测试用例导出失败", e);
            e.printStackTrace();
        } finally {
            try {
                if (os != null) {
                    os.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return true;
    }

    /**
     * 递归 ExportDTO，取得需要导出的所有数据（找到最后的叶子节点，然后再递归查找分类下的用例）
     * @param catalogName 目录名称
     * @param FullTreeList 目录下的子目录
     * @param exportData 导出数据
     * @return
     */
    private boolean recursiveExportDTO(String catalogName, List<FullTree> FullTreeList, Map<String, List<TestCase>> exportData) {
        for (FullTree fullTree : FullTreeList) {
            Catalog catalog = catalogDao.selectById(fullTree.getId()); // 查找目录信息
            if (catalog == null) {
                throw new RuntimeException("导出测试用例，目录不存在。catalogId:" + fullTree.getId());
            }

            // 如果有子节点，继续递归
            if (fullTree.getChildren()!= null && fullTree.getChildren().size() > 0) {
                if (catalogName.length() == 0) {
                    recursiveExportDTO(catalog.getName(), fullTree.getChildren(), exportData);
                } else {
                    recursiveExportDTO(catalogName + "/" + catalog.getName(), fullTree.getChildren(), exportData);
                }
            } else {
                recursiveCatalog(catalogName, catalog, exportData); // 最后的叶子节点，递归分类（递归查找分类下的用例）
            }
        }

        return true;
    }

    /**
     * 递归分类（递归查找分类下的用例）
     * @param catalogName 目录名称
     * @param catalog 目录
     * @param exportData 导出数据
     */
    private Boolean recursiveCatalog(String catalogName, Catalog catalog, Map<String, List<TestCase>> exportData) {
        // 遍历递归目录下的子目录
        for (Catalog tmp : catalogDao.listCatalogByParentId(CatalogType.TEST_CASE, catalog.getProjectId(), catalog.getId())) {
            if (catalogName.length() == 0) {
                recursiveCatalog(catalog.getName(), tmp, exportData);
            } else {
                recursiveCatalog(catalogName + "/" + catalog.getName(), tmp, exportData);
            }
        }

        List<TestCase> testCaseList = testCaseDao.selectByCatalogId(catalog.getProjectId(), catalog.getType(), catalog.getId()); // 查询目录下的用例

        // 将目录下的用例放入导出数据中
        if (catalogName.length() == 0) {
            exportData.put(catalog.getName(), testCaseList);
        } else {
            exportData.put(catalogName + "/" + catalog.getName(), testCaseList);
        }

        return true;
    }

    @Override
    public List<TestCase> search(SearchDTO searchDTO) {
        return testCaseDao.search(searchDTO.getProjectId(), searchDTO.getType(), searchDTO.getFindStr(), "%" + searchDTO.getFindStr() + "%");
    }

    /**
     * 导入测试用例，组装数据
     * @param excelRead excelRead
     * @param caseList 测试用例列表
     * @param stepList 步骤列表
     * @param projectId 项目id
     * @param userName 用户名
     * @throws ExcelException
     */
    private void fileImportAssemblyData(ExcelRead excelRead, List<TestCase> caseList, List<Step> stepList, Long projectId, String userName) throws ExcelException {
        String catalog = excelRead.getString("用例分类");
        String caseNumber = excelRead.getString("用例编号");
        String caseName = excelRead.getString("用例名称");
        String before = excelRead.getString("前置条件");
        String designer = excelRead.getString("设计者");
        String caseStep = excelRead.getString("操作步骤");
        String caseSulit = excelRead.getString("预期结果");

        // 全空跳过
        if (StringUtils.isEmpty(catalog) && StringUtils.isEmpty(caseNumber) && StringUtils.isEmpty(caseName) && StringUtils.isEmpty(before) && StringUtils.isEmpty(designer) && StringUtils.isEmpty(caseStep) && StringUtils.isEmpty(caseSulit)) {
            return;
        }

        // 校验数据
        StringBuffer errorMsg = new StringBuffer();
        if (caseNumber.length() > 50) {
            errorMsg.append("“用例编号”长度超过50个字符。");
        }
        if (StringUtils.isEmpty(caseName) || caseName.length() > 150) {
            errorMsg.append("“用例名称”为空或长度超过150个字符。");
        }
        if (before.length() > 500) {
            errorMsg.append("“前置条件”长度超过500个字符。");
        }
        if (designer.length() > 50) {
            errorMsg.append("“设计者”长度超过50个字符。");
        }
        if (caseStep.length() > 65534) {
            errorMsg.append("“操作步骤”长度超过65534个字符。");
        }
        if (caseSulit.length() > 65534) {
            errorMsg.append("“预期结果”长度超过65534个字符。");
        }
        if (errorMsg.length() > 0) {
            throw new ExcelException(errorMsg.toString());
        }

        // 组装 testList
        TestCase tmpTestCase = new TestCase();
        tmpTestCase.setNumber(caseNumber);
        tmpTestCase.setName(caseName);
        tmpTestCase.setBefore(before);
        String priority = excelRead.getString("优先级"); // 优先级转枚举，报错改成P2
        try {
            tmpTestCase.setPriority(Priority.valueOf(priority).getValue());
        } catch (IllegalArgumentException e) {
            tmpTestCase.setPriority(Priority.P2.getValue());
        }
        if (StringUtils.isBlank(designer)) {
            tmpTestCase.setDesigner(userName);
        } else {
            tmpTestCase.setDesigner(designer);
        }
        tmpTestCase.setType(CatalogType.TEST_CASE.getValue());
        tmpTestCase.setCaseType(CatalogType.TEST_CASE.getValue());
        tmpTestCase.setProjectId(projectId);
        tmpTestCase.setEditStatus(EditStatus.WAIT_REVIEW.getValue());
        caseList.add(tmpTestCase);

        // 组装 stepList
        Step tmpStep = new Step();
        tmpStep.setProjectId(projectId);
        tmpStep.setExtra1(caseStep);
        tmpStep.setExtra2(caseSulit);
        stepList.add(tmpStep);
    }

    /**
     * 导入测试用例，保存数据
     * @param currentCatalog 当前分类
     * @param projectId 项目id
     * @param caseList 测试用例列表
     * @param stepList 步骤列表
     */
    private void fileImportSaveData(String currentCatalog, Long projectId, List<TestCase> caseList, List<Step> stepList) {
        Long pId = 0L;
        if (StringUtils.isNotEmpty(currentCatalog)) {
            for (String cName : currentCatalog.split("/")) { // 循环查找或创建分类
                if (StringUtils.isNotEmpty(cName)) { // 非空才处理分类，避免这三种 bug "/aaa"、"aaa/"、"aaa//bbb"
                    Catalog temp = iCatalogService.selectByName(CatalogType.TEST_CASE, projectId, pId, cName);
                    if (temp == null) { // 不存在则插入，既然是 null 也就不需要在创建新变量了
                        temp = new Catalog();
                        temp.setProjectId(projectId);
                        temp.setType(CatalogType.TEST_CASE.getValue());
                        temp.setParentId(pId);
                        temp.setName(cName);
                        iCatalogService.save(temp);
                    }
                    pId = temp.getId();
                }
            }
        }

        // 批量保存 testCase
        for (TestCase testCase : caseList) {
            testCase.setCatalogId(pId);
        }
        saveBatch(caseList);

        // 批量保存 step
        for (int i= 0; i < caseList.size(); i++) {
            stepList.get(i).setTestCaseId(caseList.get(i).getId());
        }
        iStepService.saveBatch(stepList);
    }

    @Override
    public TestResultSuite testCaseDebug(TestCaseDebugDTO testCaseDebugDTO, String runUser) {
        TestCase testCase = testCaseDebugDTO.getTestCase();
        if (testCase == null || testCase.getId() == null || testCase.getId() == 0L) {
            throw new MyException("测试用例 ID 不能为空，请先保存测试用例！");
        }

        // 遍历 step 设置 测试用例 ID（避免步骤没有 测试用例 ID 报错）
        for (Step step : testCase.getSteps()) {
            step.setTestCaseId(testCase.getId());
        }

        // 组装 testSuite
        TestSuite testSuite = new TestSuite();
        testSuite.setName("DEBUG_" + testCase.getName());
        testSuite.setCaseType(CatalogType.INTERFACE_TEST_CASE.getValue());
        testSuite.setProjectId(testCaseDebugDTO.getProjectId());
        testSuite.setEnv(testCaseDebugDTO.getEnv());
        testSuite.setRobotId(null);
        testSuite.setId(-9999L);

        // 组装待运行的测试用例
        List<TestCase> testCaseList = new ArrayList<>();
        testCaseList.add(testCase);

        // 组装待运行的 agent
        List<Agent> agentList = new ArrayList<>();
        Agent agent = null;
        if (testCaseDebugDTO.getAgentId() == 0) {
            agent = iAgentService.getOnlineAgent();
            if (agent == null) {
                throw new MyException("没有在线的 agent，无法调试！");
            }
        } else {
            agent = agentDao.selectById(testCaseDebugDTO.getAgentId());
            if (agent.getStatus() == AgentStatus.OFF_LINE) {
                throw new MyException("您选取的「" + agent.getName() + "」agent 不在线，无法调试！");
            }
        }
        agentList.add(agent);

        return iTestSuiteService.runSuite(testSuite, testCaseList, agentList, null, runUser);
    }
}
